"use strict";

declare var global: any;

/* tslint:disable no-require-imports */

import * as fs from "fs";
import * as glob from "glob";
import * as paths from "path";
import * as Mocha from "mocha";
// import * as NYC from "nyc";
import { NYC, libHook } from "nyc";
const remapIstanbul = require("remap-istanbul");


// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY
// Since we are not running in a tty environment, we just implementt he method statically
const tty = require("tty");
if (!tty.getWindowSize)
{
    tty.getWindowSize = function (): number[]
    {
        return [80, 75];
    };
}

let mocha = new Mocha({
    ui: "tdd"
});

let testOptions: any;

export function configure(mochaOpts: Mocha.MochaOptions, testOpts: any): void
{
    // For Mocha >= 6.0.0, `useColors` is deprecated
    const color = mochaOpts.useColors || (<any>mochaOpts).color || false;
    delete mochaOpts.useColors;

    mocha = new Mocha(mochaOpts);
    mocha.useColors(color);
    testOptions = testOpts;
}


function _mkDirIfExists(dir: string): void
{
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
    }
}


function _readCoverOptions(testsRoot: string): ITestRunnerOptions | undefined {
    const coverConfigPath = paths.join(testsRoot, "..", "..", "coverconfig.json");
    if (fs.existsSync(coverConfigPath)) {
        const configContent = fs.readFileSync(coverConfigPath, "utf-8");
        return JSON.parse(configContent);
    }
    return undefined;
}


function run(testsRoot: string, clb: any): any
{
    // Read configuration for the coverage file
    const coverOptions = _readCoverOptions(testsRoot);
    if (coverOptions && coverOptions.enabled) {
        // Setup coverage pre-test, including post-test hook to report
        const coverageRunner = new CoverageRunner(coverOptions, testsRoot);
        coverageRunner.setupCoverage();
    }

    // Glob test files
    glob("**/**.test.js", { cwd: testsRoot }, (error, files): any => {
        if (error) {
            return clb(error);
        }
        try {
            // Fill into Mocha
            files.forEach((f): Mocha => mocha.addFile(paths.join(testsRoot, f)));
            // Run the tests
            let failureCount = 0;

            mocha.run()
                .on("fail", () => failureCount++)
                .on("end", () => clb(undefined, failureCount)
            );
        } catch (error) {
            return clb(error);
        }
    });
}
exports.run = run;

interface ITestRunnerOptions {
    enabled?: boolean;
    relativeCoverageDir: string;
    relativeSourcePath: string;
    ignorePatterns: string[];
    includePid?: boolean;
    reports?: string[];
    verbose?: boolean;
}

class CoverageRunner
{
    private nyc: NYC;
    private coverageVar: string = "$$cov_" + new Date().getTime() + "$$";
    private transformer: any = undefined;
    private matchFn: any = undefined;
    private instrumenter: any = undefined;

    constructor(private options: ITestRunnerOptions, private testsRoot: string)
    {
        if (!options.relativeSourcePath) {
            return;
        }
        this.nyc = new NYC(options);
    }

    public setupCoverage(): void
    {
        // Set up Code Coverage, hooking require so that instrumented code is returned
        const self = this;
        //self.instrumenter = new this.nyc.instrumenter({ coverageVariable: self.coverageVar });
        self.instrumenter = this.nyc.instrumenter();
        const sourceRoot = paths.join(self.testsRoot, self.options.relativeSourcePath);

        // Glob source files
        const srcFiles = glob.sync("**/**.js", {
            cwd: sourceRoot,
            ignore: self.options.ignorePatterns,
        });

        // Create a match function - taken from the run-with-cover.js in istanbul.
        const decache = require("decache");
        const fileMap: any = {};
        srcFiles.forEach((file) => {
            const fullPath = paths.join(sourceRoot, file);
            fileMap[fullPath] = true;

            // On Windows, extension is loaded pre-test hooks and this mean we lose
            // our chance to hook the Require call. In order to instrument the code
            // we have to decache the JS file so on next load it gets instrumented.
            // This doesn't impact tests, but is a concern if we had some integration
            // tests that relied on VSCode accessing our module since there could be
            // some shared global state that we lose.
            decache(fullPath);
        });

        self.matchFn = (file: string): boolean => fileMap[file];
        self.matchFn.files = Object.keys(fileMap);

        // Hook up to the Require function so that when this is called, if any of our source files
        // are required, the instrumented version is pulled in instead. These instrumented versions
        // write to a global coverage variable with hit counts whenever they are accessed
        self.transformer = self.instrumenter.instrumentSync.bind(self.instrumenter);
        const hookOpts = { verbose: false, extensions: [".js"] };


        //
        // TODO!!!
        //
        // self.libHook.hookRequire(self.matchFn, self.transformer, hookOpts);



        // initialize the global variable to stop mocha from complaining about leaks
        global[self.coverageVar] = {};

        // Hook the process exit event to handle reporting
        // Only report coverage if the process is exiting successfully
        process.on("exit", async (code: number) => {
            self.reportCoverage();
            process.exitCode = code;
        });
    }


    public async reportCoverage() {
        const self = this;
        
        
        //
        // TODO!!!
        //
        // self.nyc.hook.unhookRequire();


        
        let cov: any;
        if (typeof global[self.coverageVar] === "undefined" || Object.keys(global[self.coverageVar]).length === 0) {
            console.error("No coverage information was collected, exit without writing coverage information");
            return;
        } else {
            cov = global[self.coverageVar];
        }

        // TODO consider putting this under a conditional flag
        // Files that are not touched by code ran by the test runner is manually instrumented, to
        // illustrate the missing coverage.
        self.matchFn.files.forEach((file: any) => {
            if (cov[file]) {
                return;
            }
            self.transformer(fs.readFileSync(file, "utf-8"), file);

            // When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s,
            // presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted,
            // as it was never loaded.
            Object.keys(self.instrumenter.coverState.s).forEach((key) => {
                self.instrumenter.coverState.s[key] = 0;
            });

            cov[file] = self.instrumenter.coverState;
        });

        // TODO Allow config of reporting directory with
        const reportingDir = paths.join(self.testsRoot, self.options.relativeCoverageDir);
        const includePid = self.options.includePid;
        const pidExt = includePid ? ("-" + process.pid) : "";
        const coverageFile = paths.resolve(reportingDir, "coverage" + pidExt + ".json");

        // yes, do this again since some test runners could clean the dir initially created
        _mkDirIfExists(reportingDir);

        fs.writeFileSync(coverageFile, JSON.stringify(cov), "utf8");

        const remappedCollector = remapIstanbul.remap(cov, {
            warn: (warning: any) => {
                // We expect some warnings as any JS file without a typescript mapping will cause this.
                // By default, we'll skip printing these to the console as it clutters it up
                if (self.options.verbose) {
                    console.warn(warning);
                }
            }
        });

        await this.nyc.writeCoverageFile();

        //
        // TODO!!!
        //
        // const reporter = new self.nyc.Reporter(undefined, reportingDir);
        // const reportTypes = (self.options.reports instanceof Array) ? self.options.reports : ["lcov"];
        // reporter.addAll(reportTypes);
        // reporter.write(remappedCollector, true, () => {
        //     console.log(`reports written to ${reportingDir}`);
        // });
    }
}
